import os
import re
import time
from datetime import datetime
from typing import Dict, List, Optional

import requests
import xlwt
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class Music:
    """豆瓣音乐数据类，统一封装排行榜条目"""

    def __init__(self, rank: int = 0):
        self.rank = rank  # 排名
        self.title: str = ""  # 专辑名称
        self.url: str = ""  # 豆瓣链接
        self.img_url: str = ""  # 封面图片
        self.rating: float = 0.0  # 评分
        self.rating_count: str = ""  # 评价人数
        self.performers: str = ""  # 表演者
        self.genre: str = ""  # 流派
        self.release_date: str = ""  # 发行时间
        self.img_local_path: str = ""  # 本地封面路径
        self.download_success: bool = False  # 封面是否下载成功

    def to_dict(self) -> Dict:
        """转换为便于写入Excel的字典"""
        return {
            '排名': self.rank,
            '专辑名': self.title,
            '音乐链接': self.url,
            '封面URL': self.img_url,
            '评分': self.rating,
            '评价人数': self.rating_count,
            '表演者': self.performers,
            '流派': self.genre,
            '发行时间': self.release_date,
            '本地封面路径': self.img_local_path,
            '封面下载状态': '成功' if self.download_success else '失败'
        }

    def __str__(self) -> str:
        """美化输出音乐信息"""
        return f"""
🎵 音乐 #{self.rank}
💿 专辑: {self.title}
📎 链接: {self.url}
🎤 表演者: {self.performers or '未知'}
🎼 流派: {self.genre or '未知'}
📅 发行时间: {self.release_date or '未知'}
⭐ 评分: {self.rating} ({self.rating_count})
🖼️ 封面: {'✅' if self.download_success else '❌'} {os.path.basename(self.img_local_path) if self.img_local_path else '无文件'}
        """


class DoubanMusicSeleniumSpider:
    """Selenium + requests + BeautifulSoup 版豆瓣音乐爬虫"""

    def __init__(self, headless: bool = True):
        self.base_url = 'https://www.douban.com/doulist/158870218/'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
        }
        self.page_size = 25
        self.img_folder = os.path.join(os.getcwd(), 'music_covers_selenium')
        self.musics: List[Music] = []

        self.driver = None
        self.wait = None
        self._setup_selenium(headless)

    def _setup_selenium(self, headless: bool) -> None:
        """配置Selenium WebDriver"""
        chrome_options = Options()
        if headless:
            chrome_options.add_argument('--headless')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--disable-gpu')
        chrome_options.add_argument('--window-size=1920,1080')
        chrome_options.add_argument(f'--user-agent={self.headers["User-Agent"]}')

        try:
            self.driver = webdriver.Chrome(options=chrome_options)
            self.wait = WebDriverWait(self.driver, 12)
            print("🤖 Selenium WebDriver 初始化成功")
        except Exception as exc:
            print(f"⚠️ Selenium 初始化失败: {exc}")
            print("💡 请确认chromedriver版本与Chrome浏览器匹配")
            raise

    def close_driver(self) -> None:
        """释放Selenium资源"""
        if self.driver:
            try:
                self.driver.quit()
                print("🔚 Selenium WebDriver 已关闭")
            except Exception as exc:
                print(f"⚠️ 关闭WebDriver时出现异常: {exc}")

    def scrape_music(self, max_pages: int = 5) -> List[Music]:
        """通过Selenium加载页面，再交给BeautifulSoup解析"""
        print("🚀 使用Selenium抓取豆瓣华语音乐Top250...")
        print(f"📄 计划爬取页数: {max_pages}")
        current_page = 1
        current_rank = 1

        while current_page <= max_pages:
            page_url = self._build_page_url(current_page)
            print(f"📄 Selenium打开第{current_page}页 -> {page_url}")

            try:
                self.driver.get(page_url)
                self.wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'div.doulist-item')))
            except TimeoutException:
                print(f"⚠️ 第{current_page}页加载超时，跳过")
                current_page += 1
                continue

            soup = BeautifulSoup(self.driver.page_source, 'html.parser')
            music_blocks = soup.select('div.doulist-item')
            print(f"📊 第{current_page}页抓取到 {len(music_blocks)} 条记录")

            if not music_blocks:
                print("ℹ️ 未找到更多条目，提前结束")
                break

            for block in music_blocks:
                music = self._extract_music_info(block, current_rank)
                if music:
                    self.musics.append(music)
                    current_rank += 1

            current_page += 1
            time.sleep(1.5)

        print(f"🎉 Selenium抓取结束，共获取 {len(self.musics)} 条数据")
        return self.musics

    def _build_page_url(self, page: int) -> str:
        if page <= 1:
            return self.base_url
        offset = (page - 1) * self.page_size
        return f"{self.base_url}?start={offset}&sort=seq&sub_type="

    def _extract_music_info(self, block, rank: int) -> Optional[Music]:
        """从一个条目中解析音乐信息"""
        try:
            music = Music(rank)

            title_link = block.select_one('div.title a')
            if title_link:
                music.title = title_link.get_text(strip=True)
                music.url = title_link['href'].strip()

            img_element = block.select_one('div.post img')
            if img_element:
                music.img_url = img_element.get('src', '').strip()

            rating_element = block.select_one('div.rating span.rating_nums')
            if rating_element:
                try:
                    music.rating = float(rating_element.get_text(strip=True))
                except ValueError:
                    music.rating = 0.0

            music.rating_count = self._extract_rating_count(block.select_one('div.rating'))

            abstract_element = block.select_one('div.abstract')
            details = self._parse_abstract(abstract_element)
            music.performers = details.get('表演者', '')
            music.genre = details.get('流派', '')
            music.release_date = details.get('发行时间', '')

            return music
        except Exception as exc:
            print(f"⚠️ 解析排名#{rank} 音乐失败: {exc}")
            return None

    def _extract_rating_count(self, rating_block) -> str:
        if not rating_block:
            return ""
        text = rating_block.get_text(" ", strip=True)
        match = re.search(r'(\d+人评价)', text)
        return match.group(1) if match else ""

    def _parse_abstract(self, abstract_element) -> Dict[str, str]:
        details: Dict[str, str] = {}
        if not abstract_element:
            return details
        for raw_line in abstract_element.stripped_strings:
            normalized = raw_line.replace('：', ':')
            if ':' not in normalized:
                continue
            label, value = normalized.split(':', 1)
            details[label.strip()] = value.strip()
        return details

    def download_covers(self, musics: Optional[List[Music]] = None) -> None:
        if musics is None:
            musics = self.musics
        if not musics:
            print("ℹ️ 没有音乐数据，不执行封面下载")
            return

        os.makedirs(self.img_folder, exist_ok=True)
        print(f"🖼️ 开始下载封面 -> {self.img_folder}")

        for music in musics:
            if not music.img_url:
                continue
            music.download_success = self._download_cover(music)

    def _download_cover(self, music: Music) -> bool:
        try:
            response = requests.get(music.img_url, headers=self.headers, timeout=15)
            if response.status_code != 200:
                return False
            filename = self._clean_filename(f"{music.rank:03d}_{music.title}.jpg")
            local_path = os.path.join(self.img_folder, filename)
            with open(local_path, 'wb') as file:
                file.write(response.content)
            music.img_local_path = local_path
            return True
        except Exception as exc:
            print(f"⚠️ 下载封面失败 ({music.title}): {exc}")
            return False

    def _clean_filename(self, filename: str) -> str:
        cleaned = re.sub(r'[<>:\"/\\|?*]', '', filename)
        return cleaned[:80]

    def export_to_excel(self, musics: Optional[List[Music]] = None, filename: Optional[str] = None) -> str:
        if musics is None:
            musics = self.musics
        if not musics:
            print("ℹ️ 没有音乐数据可导出")
            return ""
        if filename is None:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"豆瓣音乐Top250_Selenium_{timestamp}.xls"

        workbook = xlwt.Workbook(encoding='utf-8')
        worksheet = workbook.add_sheet('豆瓣音乐Top250')
        headers = ['排名', '专辑名', '音乐链接', '封面URL', '评分', '评价人数',
                   '表演者', '流派', '发行时间', '本地封面路径', '封面下载状态']

        for col, header in enumerate(headers):
            worksheet.write(0, col, header)

        for row, music in enumerate(musics, start=1):
            music_data = music.to_dict()
            for col, header in enumerate(headers):
                worksheet.write(row, col, str(music_data.get(header, '')))

        excel_path = os.path.join(os.getcwd(), filename)
        workbook.save(excel_path)
        print(f"📄 Excel已生成: {excel_path}")
        return excel_path

    def print_music_info(self, musics: Optional[List[Music]] = None) -> None:
        if musics is None:
            musics = self.musics
        if not musics:
            print("ℹ️ 暂无音乐数据")
            return
        print("\n" + "=" * 80)
        print("🎵 豆瓣华语音乐Top250 信息汇总 (Selenium)")
        print("=" * 80)
        for music in musics:
            print(music)
            print("-" * 80)


def main():
    print("🎵 豆瓣音乐Top250爬虫 (Selenium + BeautifulSoup)")
    print("=" * 60)
    try:
        max_pages = int(input("请输入要爬取的页数(默认3): ").strip() or "3")
        headless_input = input("是否启用无头模式?(y/n，默认y): ").strip().lower()
        headless = headless_input != 'n'
    except Exception:
        max_pages = 3
        headless = True

    spider = None
    try:
        spider = DoubanMusicSeleniumSpider(headless=headless)
        musics = spider.scrape_music(max_pages=max_pages)
        if not musics:
            print("⚠️ 未获取到音乐数据，程序结束")
            return

        # spider.download_covers()
        spider.print_music_info()
        excel_file = spider.export_to_excel()

        success_count = sum(1 for music in musics if music.download_success)
        print("\n📊 运行统计")
        print(f"🎧 总音乐数: {len(musics)}")
        print(f"🖼️ 封面下载成功: {success_count}/{len(musics)}")
        print(f"📄 Excel文件: {excel_file if excel_file else '未生成'}")
        print("\n🎉 程序执行完毕")
    except Exception as exc:
        print(f"⚠️ 程序运行失败: {exc}")
    finally:
        if spider:
            spider.close_driver()


if __name__ == "__main__":
    main()
